#!/usr/bin/env python
#coding=utf-8

import os
import sys
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, PlainTextResponse, Response
from fastapi.middleware.cors import CORSMiddleware
import mimetypes
import urllib.parse

# Add the project root to Python path
sys.path.append(os.path.dirname(os.path.realpath(__file__)))

import config
from rest.products import Products
from rest.request_handler import OhosArchInfoRequestHandler

app = FastAPI(title="Macho Inspector API", version="1.0.0")

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:8081", "http://127.0.0.1:8081", "http://localhost:8080", "http://127.0.0.1:8080"],  # 前端地址
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Initialize the products manager
products_mgr = Products()

# Mount static files
if os.path.exists(config.HTML_PATH):
    app.mount("/static", StaticFiles(directory=config.HTML_PATH), name="static")

@app.get("/", response_class=HTMLResponse)
async def read_root():
    """Serve the main index page"""
    index_path = os.path.join(config.HTML_PATH, "index.html")
    if os.path.exists(index_path):
        with open(index_path, 'r', encoding='utf-8') as f:
            return HTMLResponse(content=f.read())
    return HTMLResponse(content="<h1>Macho Inspector Service</h1><p>Service is running</p>")

@app.get("/symdb/products")
async def get_products():
    """Get all available products"""
    try:
        from rest.utils.rester import ResterHelper
        xargs = {}
        content = ResterHelper.build_array_content(products_mgr.get_all(), xargs)
        return PlainTextResponse(content=content, media_type="text/plain")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/symdb/products/{product_name}/{product_version}/{resource}")
async def get_product_resource(product_name: str, product_version: str, resource: str, request: Request):
    """Handle product-specific resource requests"""
    try:
        # Parse query parameters
        query_params = dict(request.query_params)

        # Get the product
        product = products_mgr.get_product_by_name(product_name, product_version)
        if not product:
            raise HTTPException(status_code=404, detail="Product not found")

        # Create a mock handler for compatibility
        class MockHandler:
            def __init__(self):
                self.response_content = None
                self.headers = {}

            def resStrContent(self, content, xargs=None):
                self.response_content = content
                # Determine content type based on format
                if xargs and "format" in xargs:
                    if xargs["format"] == "svg":
                        self.headers["content-type"] = "image/svg+xml"
                    elif xargs["format"] == "xml":
                        self.headers["content-type"] = "text/xml"
                        self.headers["content-disposition"] = f'attachment; filename="{product_name}_{product_version}_{resource}.xml"'

        mock_handler = MockHandler()
        path_parts = [config.REST_PREFIX, product_name, product_version, resource]

        # Process the request using existing logic
        product.doGetRequest(mock_handler, path_parts, query_params)

        if mock_handler.response_content:
            return Response(
                content=mock_handler.response_content,
                media_type=mock_handler.headers.get("content-type", "text/plain"),
                headers={
                    k: v for k, v in mock_handler.headers.items()
                    if k != "content-type"
                }
            )
        else:
            raise HTTPException(status_code=404, detail="Resource not found")

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/symdb/products/{product_name}/{product_version}/{resource}/{resource_id}")
async def get_product_resource_item(product_name: str, product_version: str, resource: str, resource_id: str, request: Request):
    """Handle specific product resource item requests"""
    try:
        # Parse query parameters
        query_params = dict(request.query_params)

        # Get the product
        product = products_mgr.get_product_by_name(product_name, product_version)
        if not product:
            raise HTTPException(status_code=404, detail="Product not found")

        # Create a mock handler for compatibility
        class MockHandler:
            def __init__(self):
                self.response_content = None
                self.headers = {}

            def resStrContent(self, content, xargs=None):
                self.response_content = content
                # Determine content type based on format
                if xargs and "format" in xargs:
                    if xargs["format"] == "svg":
                        self.headers["content-type"] = "image/svg+xml"
                    elif xargs["format"] == "xml":
                        self.headers["content-type"] = "text/xml"
                        self.headers["content-disposition"] = f'attachment; filename="{product_name}_{product_version}_{resource}_{resource_id}.xml"'

        mock_handler = MockHandler()
        path_parts = [config.REST_PREFIX, product_name, product_version, resource, resource_id]

        # Process the request using existing logic
        product.doGetRequest(mock_handler, path_parts, query_params)

        if mock_handler.response_content:
            return Response(
                content=mock_handler.response_content,
                media_type=mock_handler.headers.get("content-type", "text/plain"),
                headers={
                    k: v for k, v in mock_handler.headers.items()
                    if k != "content-type"
                }
            )
        else:
            raise HTTPException(status_code=404, detail="Resource item not found")

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/symdb/products/{product_name}/{product_version}/{resource}/{resource_id}/{operation}")
async def get_product_resource_operation(product_name: str, product_version: str, resource: str, resource_id: str, operation: str, request: Request):
    """Handle resource operations like modules/{id}/fields, modules/{id}/details"""
    try:
        # Parse query parameters
        query_params = dict(request.query_params)

        # Get the product
        product = products_mgr.get_product_by_name(product_name, product_version)
        if not product:
            raise HTTPException(status_code=404, detail="Product not found")

        # Create a mock handler for compatibility
        class MockHandler:
            def __init__(self):
                self.response_content = None
                self.headers = {}

            def resStrContent(self, content, xargs=None):
                self.response_content = content
                # Determine content type based on format
                if xargs and "format" in xargs:
                    if xargs["format"] == "svg":
                        self.headers["content-type"] = "image/svg+xml"
                    elif xargs["format"] == "xml":
                        self.headers["content-type"] = "text/xml"
                        self.headers["content-disposition"] = f'attachment; filename="{product_name}_{product_version}_{resource}_{resource_id}_{operation}.xml"'

        mock_handler = MockHandler()

        # Include the operation in the path_parts for the REST handler
        path_parts = [config.REST_PREFIX, product_name, product_version, resource, resource_id, operation]

        # Process the request using existing logic
        product.doGetRequest(mock_handler, path_parts, query_params)

        if mock_handler.response_content:
            return Response(
                content=mock_handler.response_content,
                media_type=mock_handler.headers.get("content-type", "text/plain"),
                headers={
                    k: v for k, v in mock_handler.headers.items()
                    if k != "content-type"
                }
            )
        else:
            raise HTTPException(status_code=404, detail="Resource operation not found")

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/symdb/graphviz/{product_name}/{product_version}/{graph_type}")
async def get_graphviz(product_name: str, product_version: str, graph_type: str, request: Request):
    """Handle graphviz graph generation requests"""
    try:
        query_params = dict(request.query_params)
        query_params["format"] = query_params.get("format", "svg")
        query_params["type"] = query_params.get("type", "deps")

        # Get the product
        product = products_mgr.get_product_by_name(product_name, product_version)
        if not product:
            raise HTTPException(status_code=404, detail="Product not found")

        # Get the graphviz builder and modules manager
        graphviz_builder = products_mgr.getGraphVizBuilder()
        modules_mgr = products_mgr.get_modules_manager(product)

        # For specific module requests
        if "module" in query_params and graph_type == "modules":
            module_id = query_params.get("module")
            module_name = query_params.get("name", f"Module-{module_id}")

            # Create module object for graph generation
            module_obj = {
                "name": module_name,
                "id": module_id
            }

            # Add required parameters for GraphVizBuilder
            query_params["_cur_product"] = product
            query_params["_product_mgr"] = products_mgr
            query_params["_modules_mgr"] = modules_mgr

            # Generate graph using buildGraph method
            content = graphviz_builder.buildGraph(module_obj, "modules", query_params)
        else:
            # Fallback to existing analysis API approach
            if graph_type == "deps":
                content = graphviz_builder.plotByModulesType(modules_mgr, query_params)
            else:
                raise HTTPException(status_code=404, detail="Graph type not found")

        media_type = "text/plain"
        if query_params.get("format") == "svg":
            media_type = "image/svg+xml"
        elif query_params.get("format") == "xml":
            media_type = "text/xml"

        return Response(content=content, media_type=media_type)

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/symdb/analysis/{product_name}/{product_version}")
async def get_analysis_data(product_name: str, product_version: str, request: Request):
    """Get analysis data for the specified product"""
    try:
        query_params = dict(request.query_params)
        analysis_type = query_params.get("type", "dependency")
        depth = int(query_params.get("depth", "3"))

        # Get the product
        product = products_mgr.get_product_by_name(product_name, product_version)
        if not product:
            raise HTTPException(status_code=404, detail="Product not found")

        # Get modules manager
        modules_mgr = product.getMgr("modules")
        if not modules_mgr:
            raise HTTPException(status_code=500, detail="Modules manager not found")

        # Get modules data
        modules_data = modules_mgr.get_all()

        # Process analysis results
        analysis_results = []
        for module_obj in modules_data[:50]:  # Limit to top 50 modules for performance
            # Convert module object to dictionary/list format
            module_data = getattr(module_obj, '_fields', [])
            if len(module_data) < 15:
                continue

            module_id = module_data[0] if len(module_data) > 0 else "0"
            module_name = module_data[2] if len(module_data) > 2 else f"Module_{module_id}"
            deps_count = int(module_data[4]) if len(module_data) > 4 else 0
            symbols_count = int(module_data[5]) if len(module_data) > 5 else 0
            file_size = int(module_data[13]) if len(module_data) > 13 else 0

            # Calculate complexity based on dependencies and symbols
            complexity = min(100, max(0, (deps_count * 5) + (symbols_count // 20)))

            # Determine risk level
            if complexity > 80 or deps_count > 20:
                risk_level = "高"
            elif complexity > 60 or deps_count > 10:
                risk_level = "中"
            else:
                risk_level = "低"

            analysis_results.append({
                "module": module_name,
                "complexity": complexity,
                "memoryUsage": file_size,
                "dependencies": deps_count,
                "riskLevel": risk_level
            })

        # Get graph data for dependency visualization
        try:
            graphviz_builder = products_mgr.getGraphVizBuilder()
            # Use existing method to generate dependency graph
            xargs = {
                "format": "dot",
                "type": "deps",
                "_cur_product": product,
                "_product_mgr": products_mgr
            }
            graph_content = graphviz_builder.plotByModulesType(modules_mgr, xargs)
            if not graph_content:
                # Fallback: generate simple DOT format
                graph_content = "digraph dependencies {\n"
                for module in analysis_results[:10]:
                    graph_content += f'  "{module["module"]}";\n'
                graph_content += "}"
        except Exception as e:
            print(f"Graph generation failed: {e}")
            graph_content = "digraph dependencies {\n  label=\"Graph generation failed\";\n}"

        # Return structured data
        from fastapi.responses import JSONResponse
        return JSONResponse({
            "data": {
                "analysisResults": analysis_results,
                "graphData": graph_content,
                "summary": {
                    "totalModules": len(modules_data),
                    "analyzedModules": len(analysis_results),
                    "analysisType": analysis_type,
                    "depth": depth
                }
            }
        })

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/symdb/dashboard/{product_name}/{product_version}")
async def get_dashboard_data(product_name: str, product_version: str):
    """Get dashboard statistics and data for the specified product"""
    try:
        # Get the product
        product = products_mgr.get_product_by_name(product_name, product_version)
        if not product:
            raise HTTPException(status_code=404, detail="Product not found")

        # Get modules manager
        modules_mgr = product.getMgr("modules")
        if not modules_mgr:
            raise HTTPException(status_code=500, detail="Modules manager not found")

        # Get modules data using the same method as the modules API
        from rest.utils.rester import ResterHelper
        xargs = {}
        modules_content = ResterHelper.build_array_content(modules_mgr.get_all(), xargs)

        # The content returned by ResterHelper is a JSON object with a "data" field
        import json
        try:
            parsed_content = json.loads(modules_content) if modules_content and modules_content.strip() else {}
            modules_data = parsed_content.get("data", [])
        except json.JSONDecodeError:
            modules_data = []

        # Calculate statistics
        total_modules = len(modules_data)
        total_deps = 0
        total_size = 0
        dylib_count = 0
        framework_count = 0
        executable_count = 0

        # Module statistics for charts and top modules
        module_stats = []

        for module_data in modules_data:
            if len(module_data) < 15:
                continue

            module_id = module_data[0] if len(module_data) > 0 else "0"
            module_name = module_data[2] if len(module_data) > 2 else f"Module_{module_id}"
            deps_count = int(module_data[3]) if len(module_data) > 3 else 0
            symbols_count = int(module_data[4]) if len(module_data) > 4 else 0
            file_size = int(module_data[13]) if len(module_data) > 13 else 0

            total_deps += deps_count
            total_size += file_size

            # Count module types
            if '.dylib' in module_name:
                dylib_count += 1
            elif 'framework' in module_name or 'UIKit' in module_name or 'Foundation' in module_name:
                framework_count += 1
            else:
                executable_count += 1

            # Add to module statistics
            module_stats.append({
                "id": module_id,
                "name": module_name,
                "deps": deps_count,
                "size": file_size,
                "symbols": symbols_count
            })

        # Sort by dependencies for top modules
        top_modules = sorted(module_stats, key=lambda x: x['deps'], reverse=True)[:5]

        # Sort by size for module size chart, but remove duplicates by name
        # First, group by module name and keep the maximum size for each name
        unique_modules = {}
        for module in module_stats:
            name = module["name"]
            size = module["size"]
            if name not in unique_modules or size > unique_modules[name]["size"]:
                unique_modules[name] = module

        # Then sort the unique modules by size
        top_size_modules = sorted(unique_modules.values(), key=lambda x: x['size'], reverse=True)[:10]

        # Format sizes for display
        formatted_top_modules = []
        for module in top_modules:
            formatted_top_modules.append({
                "name": module["name"],
                "deps": module["deps"],
                "size": module["size"]
            })

        formatted_size_modules = []
        for module in top_size_modules:
            formatted_size_modules.append({
                "name": module["name"],
                "size": module["size"] / (1024 * 1024)  # Convert to MB
            })

        # Calculate percentage changes (mock data for trend)
        trend_data = {
            "totalModules_trend": 12,
            "totalDeps_trend": 8,
            "totalSize_trend": -5,
            "dylibCount_trend": 15
        }

        # Return dashboard data
        from fastapi.responses import JSONResponse
        return JSONResponse({
            "data": {
                "overviewStats": {
                    "totalModules": total_modules,
                    "totalDeps": total_deps,
                    "totalSize": total_size,
                    "dylibCount": dylib_count,
                    "frameworkCount": framework_count,
                    "executableCount": executable_count,
                    "trends": trend_data
                },
                "moduleTypeDistribution": {
                    "dylibs": dylib_count,
                    "frameworks": framework_count,
                    "executables": executable_count
                },
                "topModules": formatted_top_modules,
                "topSizeModules": formatted_size_modules,
                "moduleStats": module_stats
            }
        })

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

# Catch-all handler for static files and fallback
@app.get("/{path:path}")
@app.head("/{path:path}")
async def catch_all(path: str, request: Request):
    """Serve static files or return 404"""
    # Try to serve as static file
    file_path = os.path.join(config.HTML_PATH, path)
    if os.path.exists(file_path) and os.path.isfile(file_path):
        # Determine media type
        _, ext = os.path.splitext(file_path)
        if ext == '.js':
            media_type = "application/javascript"
        elif ext == '.css':
            media_type = "text/css"
        else:
            media_type = mimetypes.types_map.get(ext, "text/plain")

        # Get file size for Content-Length header
        file_size = os.path.getsize(file_path)

        # Handle HEAD request - return headers only
        if request.method == "HEAD":
            return Response(
                content=None,
                media_type=media_type,
                headers={"Content-Length": str(file_size)}
            )

        # Handle GET request - return full content
        with open(file_path, 'rb') as f:
            content = f.read()

        return Response(content=content, media_type=media_type)

    # Check if this is a request for static assets (js, css, images, etc.)
    # If so, return 404 instead of serving index.html
    _, ext = os.path.splitext(path.lower())
    static_extensions = {'.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'}

    if ext in static_extensions:
        # For static assets that don't exist, return 404 immediately
        raise HTTPException(status_code=404, detail="Static file not found")

    # For other requests (like HTML pages), try to serve index.html for SPA routing
    index_path = os.path.join(config.HTML_PATH, "index.html")
    if os.path.exists(index_path):
        # Handle HEAD request for index.html
        if request.method == "HEAD":
            file_size = os.path.getsize(index_path)
            return Response(
                content=None,
                media_type="text/html",
                headers={"Content-Length": str(file_size)}
            )

        # Handle GET request for index.html
        with open(index_path, 'r', encoding='utf-8') as f:
            return HTMLResponse(content=f.read())

    raise HTTPException(status_code=404, detail="File not found")

if __name__ == "__main__":
    print(f"Starting Macho Inspector Service on {config.SERVER_IP}:{config.SERVER_PORT}")
    uvicorn.run(
        "main:app",
        host=config.SERVER_IP,
        port=config.SERVER_PORT,
        reload=False,
        log_level="info"
    )